﻿//Copyright (C) <2013>  <jonathan cleeve norton> All Rights Reserved 
//Contact jon.norton@fin-plus.co.uk website <http://www.fin-plus.co.uk/>
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using log = Logger.Logger;
using u = FinPlusUtility.Utility;

namespace FinPlusCommon
{
    //Note for IOC and DI, IOC to be used sparingly. Not too different to other packages, core differences
    //FinPlusDInject is fed by KVP abstract objects re universal model Functions these can be stored
    //in db which will contain change user and timestamp auditable. Can solve most objects primatives
    //IDict, IList plus collections of collections of etc see recursive ParamSolver methods. 
    //Class now passes for Beta but not prod Phase 3
    
    public sealed class FinPlusDInject
    {
        private enum Params { Adaptor, Id, FunctionName, TimeStamp, VersionControl, TestStates, States, CmdExecute, Dispose }
        private static volatile FinPlusDInject _instance;
        private static object _lock = new Object();
        private static Containers _containers;

        //public
        public static FinPlusDInject Get
        {
            get
            {
                if (_instance == null)
                {
                    lock (_lock)
                    {
                        if (_instance == null)
                        {
                            _instance = new FinPlusDInject();
                            _containers = new Containers();
                        }
                    }
                }

                return _instance;
            }
        }

        private Container Register(IFunctions template, u.Env env)
        {
            try
            {
                Container container;
                if (_containers.ContainsKey(template.Id))
                    return _containers[template.Id];

                _containers[template.Id] = container = new Container(template, env, template.Id);

                return container;
            }
            catch (Exception e)
            {
                throw new TemplateException("unable to register container", e);
            }
        }

        public IFunction Construct(IFunctions template, string name, u.Env env)
        {
            try
            {
                IFunction returnFunc = null;
                var container = Register(template, env);

                foreach (var component in template.Values)
                {
                    if (returnFunc == null && component.Get("Id").Value.Equals(name))
                        returnFunc = component;
                    Construct(component, container);
                }

                return returnFunc;
            }
            catch (Exception e)
            {
                log.Log(Logger.Level.Error, "NA", "NA", e.Message, e);
                return null;
            }
        }

        public void Construct(IFunction component, Container container, IDictionary<string, object> injectParams = null)
        {
            try
            {
                if (component.Object != null || component.Name.Equals(Params.VersionControl.ToString()))
                    return;

                component.Object = new Exception("not invoked");

                component.Invoke = string.Format("{0}({1})", component.Name, u.StringFromArray1d(component.ToParamArray(), ','));
                var f = component.Name.Split('.');
                component.Type = Type(container.Assemblys[f[0]], f[1]);
                var parms = SolveConstructorParams(component, injectParams);

                component.Object = Activator.CreateInstance(component.Type, parms);

                if (u.IsTypeOf<IFinPlusComp>(component.Object.GetType()))
                    ((IFinPlusComp)component.Object).ContainerId = container.LinkedId;//add ID for get regType if class requires

                InjectState(component, new Item(0, -1, Params.Id.ToString(), component[Params.Id.ToString()].ToString(), "", DateTime.Now, ""));
                InlineDock(component);

                return;
            }
            catch (Exception e)
            {
                component.Object = e;
            }
        }

        public object Object(string containerId, string componentId)
        {
            var container = _containers[containerId];
            var component = container.GetObject(componentId);
            if (component == null)
                return null;

            if(component.Object == null)
                Construct(component, container);
            
            return component.Object;
        }

        public object RegType(string containerId, string componentId, object[,] injectParams = null)
        {
            return RegType(containerId, componentId, u.Array2DToDict(injectParams));
        }

        public object RegType(string containerId, string componentId, IDictionary<string, object> injectParams)
        {
            var container = _containers[containerId];
            var component = container.GetRegType(componentId);
            if (component == null)
                return null;

            component.LinkedId = containerId;

            Construct(component, container, injectParams);

            return component.Object;
        }

        public object InvokeMethod(IFunction component, IFunction parentFunc, IList<string> exclude = null, IDictionary<string, object> injectParams = null)
        {
            try
            {
                component.Invoke = string.Format("{0}{1}", parentFunc.Name, u.StringFromArray1d(component.ToParamArray(exclude), ','));
                var methodInfo = parentFunc.Type.GetMethod(component.Name);
                var parms = SolveMethodParams(component.ToParamArray(exclude), methodInfo.GetParameters(), component.LinkedId, injectParams);
                if(parms == null)
                    throw new TemplateException(string.Format("method params do not match {0}.{1}}", component.Name)); 
 
                component.Object = methodInfo.Invoke(parentFunc.Object, parms);
            }
            catch (Exception e)
            {
                component.Object = new Exception(string.Format("IOC invoke failed {0}", component.Invoke), e);
            }

            return component.Object;
        }

        public IFunctions ExtractStates(IFunction component, string user = "", bool testStates = false, IFunctions states = null)
        {
            states = states == null ? new Functions() : states;
            user = string.IsNullOrEmpty(user) ? Environment.UserName : user;

            try
            {
                //get parent states
                var parentFuncState = States(component, user, testStates);
                if (parentFuncState != null) states.Add(parentFuncState, true, false);

                //get children 
                var model = Property(component, Params.Adaptor.ToString());
                if (model == null) return states;

                //get children state
                foreach (var childFunc in ((Functions)model).Values)
                    ExtractStates(childFunc, user, testStates, states);
            }
            catch (Exception e)
            {
                log.Log(Logger.Level.Error, "NA", "NA", e.Message, e);
            }
            return states;
        }

        public void InjectStates(IFunction component, IFunctions states)
        {
            try
            {
                //set parent states
                var Id = Property(component, Params.Id.ToString());
                if (Id == null) return;

                foreach (var state in states.Values)
                    if (Id.ToString() == state[Params.Id.ToString()].ToString())
                            InjectStates(component, state);

                //get children components
                var model = Property(component, Params.Adaptor.ToString());
                if (model == null) return;

                //set childFunc state
                foreach (var child in ((Functions)model).Values)
                    InjectStates(child, states);
            }
            catch (Exception e)
            {
                log.Log(Logger.Level.Error, "NA", "NA",  e.Message, e);
            }
        }

        public void CmdsExecute(IFunction component, IFunctions commands)
        {
            try
            {
                foreach (var evnt in commands.Values)
                    InjectCommand(component, evnt);
            }
            catch (Exception e)
            {
                log.Log(Logger.Level.Error, "NA", "NA", e.Message, e);
            }
        }

        public IFunctions Dispose(IFunctions template)
        {
            Container remove;
            _containers.TryRemove(template.Id, out remove);

            var dispose = new Functions();
            if (template.Values == null) return dispose;

            foreach (var component in template.Values)
            {
                if (component.Object != null && component.Type != null) 
                {
                    try
                    {
                        var methodInfo = component.Type.GetMethod(Params.Dispose.ToString());
                        object res = methodInfo.Invoke(component.Object, null);
                    }
                    catch (Exception e)
                    {
                        component.Object = e;
                        dispose.TryAdd(component.Id, component);
                    }
                }
            }

            return dispose;
        }

        public IFunctions Exceptions(IFunctions template)
        {
            var exceptions = new Functions();

            foreach (var component in template.Values)
                if (component.Object.GetType() == typeof(Exception))
                    exceptions.TryAdd(component.Id, component);

            return exceptions;
        }

        public IFunctions NullObjects(IFunctions template)
        {
            var nullObject = new Functions();

            foreach (var component in template.Values)
                if (component.Object == null)
                    nullObject.TryAdd(component.Id, component);

            return nullObject;
        }

        //private
        private void InlineDock(IFunction component)
        {
            try
            {
                var dockItem = component.Get("Dock");
                if (dockItem == null) return;

                foreach (var dock in u.ToJaggedArray(dockItem.Value, ';', ':'))
                {
                    var dockFunc = new Function("Dock");
                    dockFunc.Get("Arg1", dock[0]);
                    dockFunc.Get("Arg2", dock.Length > 1 ? dock[1] : dock[0]);
                    dockFunc.LinkedId = component.LinkedId;
                    InvokeMethod(dockFunc, component, null, null);
                }
            }
            catch (Exception e)
            {
                component.Object = new Exception(string.Format("IOC invoke failed dock {0}", component.Name), e);
            }
        }

        private object[] SolveConstructorParams(IFunction component, IDictionary<string, object> injectParams)
        {
            object[] parms;
            foreach (var constr in component.Type.GetConstructors())
            {
                parms = SolveParams(component.ToParamArray(), constr.GetParameters(), component.LinkedId, injectParams);
                if (parms != null)
                    return parms;
            }

            throw new TemplateException(string.Format("failed to invoke {0}", component.Name));
        }

        private object[] SolveMethodParams(object[] parms, ParameterInfo[] paramInfos, string linkedId, IDictionary<string, object> injectParams)
        {
            return SolveParams(parms, paramInfos, linkedId,  injectParams);
        }

        private object[] SolveParams(object[] parms, ParameterInfo[] paramInfos, string linkedId, IDictionary<string, object> injectParams)
        {
            try
            {
                var solvedParms = new object[paramInfos.Length];
                foreach (var paramInfo in paramInfos)
                {
                    if(injectParams != null && injectParams.ContainsKey(paramInfo.Name))
                    {
                        solvedParms[paramInfo.Position] = injectParams[paramInfo.Name];
                    }
                    else if (parms.Length <= paramInfo.Position)
                    {
                        if (!paramInfo.HasDefaultValue) return null;//try another constructor

                        solvedParms[paramInfo.Position] = (object)paramInfo.DefaultValue;//use deafult
                    }
                    else
                    {
                        solvedParms[paramInfo.Position] = SolveParam(parms[paramInfo.Position], paramInfo.ParameterType, linkedId, injectParams);
                    }
                }

                return solvedParms;
            }
            catch
            {
                return null;//try another constructor
            }
        }

        private object SolveParam(object val, Type type, string containerId, IDictionary<string, object> injectParams)
        {
            if (val.GetType().Equals(type))
                return val;

            if (type.Equals(typeof(IFinPlusComp)) || type.Equals(typeof(Object)))
            {
                var obj = Object(containerId, val.ToString());//get object
                if (obj == null)
                    obj = RegType(containerId, val.ToString(), injectParams);//no object get reg type
                return obj;
            }
            
            if (type.IsArray)
                return SolverArrayParam(val, type, containerId, injectParams);

            if (u.IsTypeOf<IDictionary>(type))
                return SolveIDictionaryParam(val, type, containerId, injectParams);

            if (u.IsTypeOf<IList>(type))
                return SolveIListParam(val, type, containerId, injectParams);

            return SolveTypeParam(val, type, true);
        }

        private object SolverArrayParam(object val, Type type, string containerId, IDictionary<string, object> injectParams)
        {
            var elementType = type.GetElementType();

            var vals = val.GetType().IsArray ? (object[])val : u.StringToArray1d(val.ToString(), ',', "{}");
            var obj = new ArrayList();

            for (int i = 0; i < vals.Length; i++)
                obj.Add(SolveParam(vals[i], elementType, containerId, injectParams));

            return obj.ToArray(elementType);
        }

        private object SolveIDictionaryParam(object val, Type type, string containerId, IDictionary<string, object> injectParams)
        {
            try
            {
                if (val.ToString().Equals(string.Empty)) return null;
                
                var args = type.GetGenericArguments();
                var keyType = args[0].UnderlyingSystemType;
                var valueType = args[1].UnderlyingSystemType;
               
                var dictType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
                var res = (IDictionary)Activator.CreateInstance(dictType);

                if(valueType.IsArray)
                    foreach(var kvp in u.StringToDictOfArrays(val.ToString(), ',', "{}"))
                        res[SolveParam(kvp.Key, keyType, containerId, injectParams)] = SolveParam(kvp.Value, valueType, containerId, injectParams);
                else
                    foreach(var kvp in u.StringToDict(val.ToString(), ',', "{}"))
                        res[SolveParam(kvp.Key, keyType, containerId, injectParams)] = SolveParam(kvp.Value, valueType, containerId, injectParams);

                return res;
            }
            catch (Exception e)
            {
                throw new TemplateException(string.Format("invalid parse {0}", val.ToString()), e);
            }
        }

        private object SolveIListParam(object val, Type type, string containerId, IDictionary<string, object> injectParams)
        {
            try
            {
                if (val.ToString().Equals(string.Empty))
                    return null;

                var array = (object[])u.StringToArray1d(val.ToString(), ',', "{}");

                var argType = type.GetGenericArguments()[0];
                var listType = typeof(List<>).MakeGenericType(argType);
                var list = (IList)Activator.CreateInstance(listType);

                foreach (var parm in array)
                    list.Add(SolveParam(parm, argType, containerId, injectParams));

                return list;
            }
            catch (Exception e)
            {
                throw new TemplateException(string.Format("invalid parse {0}", val.ToString()), e);
            }
        }

        private object SolveTypeParam(object val, Type type, bool DateToInt = false)
        {
            var type2 = val.GetType();
            if (type2.Equals(type))
                return val;

            if (DateToInt && type2.Equals(typeof(DateTime)))//Temp solution re QLProxy, QLProxy will be deprecated
                return (int)((DateTime)val).ToOADate();

            var parse = type.GetMethod("Parse", new[] { typeof(string) });
            if (parse != null)
                return parse.Invoke(null, new object[] { val });

            return System.Convert.ChangeType(val, type);
        }

        private IFunction States(IFunction component, string user, bool testStates)
        {
            try
            {
                //get id
                var Id = Property(component, Params.Id.ToString());
                if(Id == null) return null;
                 
                //get states          
                var states = Property(component, testStates ? Params.TestStates.ToString() : Params.States.ToString());
                if (states == null) return null;

                //get state component
                var stateFunc = new Function(component.Object.GetType().Name, Id.ToString(), user);
                var pos = 4;
                foreach (var state in (string[])states)
                {
                    var item = State(component, state, pos, user);
                    if (item == null) continue;
                    stateFunc.TryAdd(item);
                    pos++;
                }

                //if has state return func
                return pos > 4 ? stateFunc : null;
            }
            catch(Exception e)
            {
                log.Log(Logger.Level.Error, "NA", "NA",  e.Message, e);
                return null;
            }
        }

        private IItem State(IFunction component, string state, int pos, string user)
        {
            try
            {
                //get state value
                var value = Property(component, state);
                if (value == null) return null;

                //get state type
                var type = value.GetType().UnderlyingSystemType.Name;

                //make state item
                var item = new Item(pos, component.Id, state, value.ToString(), type, DateTime.Now, user);

                return item;
            }
            catch (Exception e)
            {
                log.Log(Logger.Level.Error, "NA", "NA", e.Message, e);
                return null;
            }
        }

        private void InjectStates(IFunction component, IFunction states)
        {
            foreach (var state in states)
                if (state.Name != Params.Id.ToString() && state.Name != Params.FunctionName.ToString() && state.Name != Params.TimeStamp.ToString())
                    InjectState(component, state);
        }

        private void InjectState(IFunction component, IItem state)
        {
            try
            {
                if (HasProperty(component, state.Name))
                {
                    var pInfo = component.Object.GetType().GetProperty(state.Name);
                    pInfo.SetValue(component.Object, state.Object, null);
                }
            }
            catch(Exception e)
            {
                log.Log(Logger.Level.Error, "NA", "NA", e.Message, e);
            }
        }

        private void InjectCommand(IFunction component, IFunction command)
        {
            var model = Property(component, Params.Adaptor.ToString());
            if (model == null) return;

            foreach (var child in ((Functions)model).Values)
            {
                var childId = Property(child, Params.Id.ToString());
                if (childId != null && childId.ToString() == command[Params.Id.ToString()].ToString())
                    InvokeCmdExecute(child, command);
                else
                    InjectCommand(child, command);
            }
        }

        private void InvokeCmdExecute(IFunction component, IFunction command)
        {
            try
            {
                var methodInfo = component.Object.GetType().GetMethod(Params.CmdExecute.ToString());
                var parms = new object[] { command };
                object res = methodInfo.Invoke(component.Object, parms);
            }
            catch (Exception e)
            {
                command.Object = e;
            }
        }

        private Type Type(Assembly assembly, string name)
        {
            foreach (var type in assembly.GetExportedTypes())
                if (type.Name == name)
                    return type;
          
            return null;
        }

        private object Property(IFunction component, string name)
        {
            try
            {
                if (!HasProperty(component, name)) return null;

                var obj = component.Object.GetType().GetProperty(name).GetValue(component.Object, null);
                return obj;
            }
            catch
            {
                return null;
            }
        }

        private bool HasProperty(IFunction component, string name)
        {
            if (component == null || component.Object == null) 
                return false;
            
            var type = component.Object.GetType();
            return type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public) != null;
        }
    }

    public class Containers : ConcurrentDictionary<string, Container> { }

    public class Container
    {
        public IFunctions Funcs {get; private set;}
        public string LinkedId {get; private set;}
        public Dictionary<string, IFunctions> RegTypes { get; private set; }
        public Dictionary<string, Assembly> Assemblys {get; private set;}
        public u.Env Env { get; private set; }

        public enum Params { VersionControl, Environment, Assemblies, RegisteredTypes, FinPlusRegTypes }

        //construct
        public Container(IFunctions template, u.Env env, string id)
        {
            Funcs = template;
            Env = env;
            LinkedId  = id;

            RegTypes = new Dictionary<string, IFunctions>();
            Assemblys = new Dictionary<string, Assembly>();

            var control = template.Get(Params.VersionControl.ToString());
            CheckEnv(control);
            AddRegTypes(control);
            AddAssemblys(control);
        }

        //public
        public IFunction GetObject(string id)
        {
            return Funcs.Get(id);
        }

        public IFunction GetRegType(string id)
        {
            var i = id.Split('.');
            if (i.Length < 2)
                return null;
            
            var regType = RegTypes[i[0]].Get(i[1]);
            regType.Object = null;
           
            return regType;
        }

        //private
        private void CheckEnv(IFunction check)
        {
            var env = check.Get(Params.Environment.ToString());
            var targetEnv = u.EnumParse<u.Env>(env.Value.ToLower(), u.Env.unknown);
            if (!targetEnv.Equals(Env))
                throw new TemplateException(string.Format("IOC target env={0} config env={1}", targetEnv.ToString(), Env.ToString())); 
        }

        private void AddRegTypes(IFunction check)
        {
            try
            {
                var regTypes = check.Get(Params.RegisteredTypes.ToString());
                if (regTypes == null)
                    return; //ok to have no reg types
                
                var path = u.GetAssemblyPath(Assembly.GetEntryAssembly());

                foreach (string regType in u.StringToArray1d(regTypes.Value, ',', "{}"))
                {
                    var file = string.Format("{0}{1}\\{2}.regtype", path, Params.FinPlusRegTypes.ToString(), regType);
                    RegTypes[regType] =new Functions(file);
                }

            }
            catch (Exception e)
            {
                throw new TemplateException(string.Format("IOC reg type foramt error", e));
            }
        }

        private void AddAssemblys(IFunction check)
        {
            try
            {
                var assemblyInfos = u.StringToDict(check.Get(Params.Assemblies.ToString()).Value, ',', "{}");
                if (assemblyInfos == null)
                    throw new TemplateException("IOC config missig assmbly list");

                var path = u.GetAssemblyPath(Assembly.GetEntryAssembly());

                foreach (var assemblyInfo in assemblyInfos)
                {
                    var targetAssembly = u.GetAssembly(path, assemblyInfo.Key);
                    if (targetAssembly != null && targetAssembly.GetName().Version.ToString().Equals(assemblyInfo.Value))
                        Assemblys[assemblyInfo.Key] = targetAssembly;
                    else
                        throw new TemplateException(string.Format("IOC cant not find reference assembly {0} v{1}", assemblyInfo.Key, assemblyInfo.Value));
                }
            }
            catch (Exception e)
            {
                throw new TemplateException(string.Format("IOC assembly foramt error", e));
            }
        }
    }
   
}

























































































































































































//Copyright2013<jnrton>AllRightsReserved 
//Contact j.nrton@finpus